Passed
Branch wavefile-reader (a5ebcc)
by Rafael S.
02:52
created

WaveFileParser.validateBitDepth_   A

Complexity

Conditions 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 3
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileParser class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import WaveFileReader from './wavefile-reader';
33
import writeString from './write-string';
34
import validateNumChannels from './validate-num-channels'; 
35
import validateSampleRate from './validate-sample-rate';
36
import {unpack, packTo, packStringTo, packString, pack} from 'byte-data';
37
38
/**
39
 * A class to read and write wav files.
40
 * @extends WaveFileReader
41
 */
42
export default class WaveFileParser extends WaveFileReader {
43
44
  constructor(wavBuffer=null) {
45
    super(wavBuffer);
46
    /**
47
     * The bit depth code according to the samples.
48
     * @type {string}
49
     */
50
    this.bitDepth = '0';
51
    /**
52
     * @type {!Object}
53
     * @protected
54
     */
55
    this.dataType = {};
56
    /**
57
     * Audio formats.
58
     * Formats not listed here should be set to 65534,
59
     * the code for WAVE_FORMAT_EXTENSIBLE
60
     * @enum {number}
61
     * @protected
62
     */
63
    this.WAV_AUDIO_FORMATS = {
64
      '4': 17,
65
      '8': 1,
66
      '8a': 6,
67
      '8m': 7,
68
      '16': 1,
69
      '24': 1,
70
      '32': 1,
71
      '32f': 3,
72
      '64': 3
73
    };
74
    if (wavBuffer) {
75
      this.bitDepthFromFmt_();
76
      this.updateDataType();
77
    }
78
  }
79
80
  /**
81
   * Set up the WaveFileParser object from a byte buffer.
82
   * @param {!Uint8Array} wavBuffer The buffer.
83
   * @param {boolean=} samples True if the samples should be loaded.
84
   * @throws {Error} If container is not RIFF, RIFX or RF64.
85
   * @throws {Error} If format is not WAVE.
86
   * @throws {Error} If no 'fmt ' chunk is found.
87
   * @throws {Error} If no 'data' chunk is found.
88
   */
89
  fromBuffer(wavBuffer, samples=true) {
90
    super.fromBuffer(wavBuffer, samples);
91
    this.bitDepthFromFmt_();
92
    this.updateDataType();
93
  }
94
95
  /**
96
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
97
   * The return value of this method can be written straight to disk.
98
   * @return {!Uint8Array} A wav file.
99
   * @throws {Error} If bit depth is invalid.
100
   * @throws {Error} If the number of channels is invalid.
101
   * @throws {Error} If the sample rate is invalid.
102
   */
103
  toBuffer() {
104
    this.validateWavHeader();
105
    return this.writeWavBuffer_();
106
  }
107
108
  /**
109
   * Return the sample at a given index.
110
   * @param {number} index The sample index.
111
   * @return {number} The sample.
112
   * @throws {Error} If the sample index is off range.
113
   */
114
  getSample(index) {
115
    index = index * (this.dataType.bits / 8);
116
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
117
      throw new Error('Range error');
118
    }
119
    return unpack(
120
      this.data.samples.slice(index, index + this.dataType.bits / 8),
121
      this.dataType);
122
  }
123
124
  /**
125
   * Set the sample at a given index.
126
   * @param {number} index The sample index.
127
   * @param {number} sample The sample.
128
   * @throws {Error} If the sample index is off range.
129
   */
130
  setSample(index, sample) {
131
    index = index * (this.dataType.bits / 8);
132
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
133
      throw new Error('Range error');
134
    }
135
    packTo(sample, this.dataType, this.data.samples, index);
136
  }
137
138
  /**
139
   * Validate the header of the file.
140
   * @throws {Error} If bit depth is invalid.
141
   * @throws {Error} If the number of channels is invalid.
142
   * @throws {Error} If the sample rate is invalid.
143
   * @ignore
144
   * @protected
145
   */
146
  validateWavHeader() {
147
    this.validateBitDepth_();
148
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
149
      throw new Error('Invalid number of channels.');
150
    }
151
    if (!validateSampleRate(
152
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
153
      throw new Error('Invalid sample rate.');
154
    }
155
  }
156
157
  /**
158
   * Update the type definition used to read and write the samples.
159
   * @protected
160
   */
161
  updateDataType() {
162
    this.dataType = {
163
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
164
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
165
      signed: this.bitDepth != '8',
166
      be: this.container == 'RIFX'
167
    };
168
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
169
      this.dataType.bits = 8;
170
      this.dataType.signed = false;
171
    }
172
  }
173
174
  /**
175
   * Set the string code of the bit depth based on the 'fmt ' chunk.
176
   * @private
177
   */
178
  bitDepthFromFmt_() {
179
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
180
      this.bitDepth = '32f';
181
    } else if (this.fmt.audioFormat === 6) {
182
      this.bitDepth = '8a';
183
    } else if (this.fmt.audioFormat === 7) {
184
      this.bitDepth = '8m';
185
    } else {
186
      this.bitDepth = this.fmt.bitsPerSample.toString();
187
    }
188
  }
189
190
  /**
191
   * Return a .wav file byte buffer with the data from the WaveFileParser object.
192
   * The return value of this method can be written straight to disk.
193
   * @return {!Uint8Array} The wav file bytes.
194
   * @private
195
   */
196
  writeWavBuffer_() {
197
    this.uInt16.be = this.container === 'RIFX';
198
    this.uInt32.be = this.uInt16.be;
199
    /** @type {!Array<!Array<number>>} */
200
    let fileBody = [
201
      this.getJunkBytes_(),
202
      this.getDs64Bytes_(),
203
      this.getBextBytes_(),
204
      this.getFmtBytes_(),
205
      this.getFactBytes_(),
206
      packString(this.data.chunkId),
207
      pack(this.data.samples.length, this.uInt32),
208
      this.data.samples,
209
      this.getCueBytes_(),
210
      this.getSmplBytes_(),
211
      this.getLISTBytes_()
212
    ];
213
    /** @type {number} */
214
    let fileBodyLength = 0;
215
    for (let i=0; i<fileBody.length; i++) {
216
      fileBodyLength += fileBody[i].length;
217
    }
218
    /** @type {!Uint8Array} */
219
    let file = new Uint8Array(fileBodyLength + 12);
220
    /** @type {number} */
221
    let index = 0;
222
    index = packStringTo(this.container, file, index);
223
    index = packTo(fileBodyLength + 4, this.uInt32, file, index);
224
    index = packStringTo(this.format, file, index);
225
    for (let i=0; i<fileBody.length; i++) {
226
      file.set(fileBody[i], index);
227
      index += fileBody[i].length;
228
    }
229
    return file;
230
  }
231
232
  /**
233
   * Return the bytes of the 'bext' chunk.
234
   * @private
235
   */
236
  getBextBytes_() {
237
    /** @type {!Array<number>} */
238
    let bytes = [];
239
    this.enforceBext_();
240
    if (this.bext.chunkId) {
241
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
242
      bytes = bytes.concat(
243
        packString(this.bext.chunkId),
244
        pack(602 + this.bext.codingHistory.length, this.uInt32),
245
        writeString(this.bext.description, 256),
246
        writeString(this.bext.originator, 32),
247
        writeString(this.bext.originatorReference, 32),
248
        writeString(this.bext.originationDate, 10),
249
        writeString(this.bext.originationTime, 8),
250
        pack(this.bext.timeReference[0], this.uInt32),
251
        pack(this.bext.timeReference[1], this.uInt32),
252
        pack(this.bext.version, this.uInt16),
253
        writeString(this.bext.UMID, 64),
254
        pack(this.bext.loudnessValue, this.uInt16),
255
        pack(this.bext.loudnessRange, this.uInt16),
256
        pack(this.bext.maxTruePeakLevel, this.uInt16),
257
        pack(this.bext.maxMomentaryLoudness, this.uInt16),
258
        pack(this.bext.maxShortTermLoudness, this.uInt16),
259
        writeString(this.bext.reserved, 180),
260
        writeString(
261
          this.bext.codingHistory, this.bext.codingHistory.length));
262
    }
263
    return bytes;
264
  }
265
266
  /**
267
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
268
   * @private
269
   */
270
  enforceBext_() {
271
    for (let prop in this.bext) {
272
      if (this.bext.hasOwnProperty(prop)) {
273
        if (this.bext[prop] && prop != 'timeReference') {
274
          this.bext.chunkId = 'bext';
275
          break;
276
        }
277
      }
278
    }
279
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
280
      this.bext.chunkId = 'bext';
281
    }
282
  }
283
284
  /**
285
   * Return the bytes of the 'ds64' chunk.
286
   * @return {!Array<number>} The 'ds64' chunk bytes.
287
   * @private
288
   */
289
  getDs64Bytes_() {
290
    /** @type {!Array<number>} */
291
    let bytes = [];
292
    if (this.ds64.chunkId) {
293
      bytes = bytes.concat(
294
        packString(this.ds64.chunkId),
295
        pack(this.ds64.chunkSize, this.uInt32),
296
        pack(this.ds64.riffSizeHigh, this.uInt32),
297
        pack(this.ds64.riffSizeLow, this.uInt32),
298
        pack(this.ds64.dataSizeHigh, this.uInt32),
299
        pack(this.ds64.dataSizeLow, this.uInt32),
300
        pack(this.ds64.originationTime, this.uInt32),
301
        pack(this.ds64.sampleCountHigh, this.uInt32),
302
        pack(this.ds64.sampleCountLow, this.uInt32));
303
    }
304
    //if (this.ds64.tableLength) {
305
    //  ds64Bytes = ds64Bytes.concat(
306
    //    pack(this.ds64.tableLength, this.uInt32),
307
    //    this.ds64.table);
308
    //}
309
    return bytes;
310
  }
311
312
  /**
313
   * Return the bytes of the 'cue ' chunk.
314
   * @return {!Array<number>} The 'cue ' chunk bytes.
315
   * @private
316
   */
317
  getCueBytes_() {
318
    /** @type {!Array<number>} */
319
    let bytes = [];
320
    if (this.cue.chunkId) {
321
      /** @type {!Array<number>} */
322
      let cuePointsBytes = this.getCuePointsBytes_();
323
      bytes = bytes.concat(
324
        packString(this.cue.chunkId),
325
        pack(cuePointsBytes.length + 4, this.uInt32),
326
        pack(this.cue.dwCuePoints, this.uInt32),
327
        cuePointsBytes);
328
    }
329
    return bytes;
330
  }
331
332
  /**
333
   * Return the bytes of the 'cue ' points.
334
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
335
   * @private
336
   */
337
  getCuePointsBytes_() {
338
    /** @type {!Array<number>} */
339
    let points = [];
340
    for (let i=0; i<this.cue.dwCuePoints; i++) {
341
      points = points.concat(
342
        pack(this.cue.points[i].dwName, this.uInt32),
343
        pack(this.cue.points[i].dwPosition, this.uInt32),
344
        packString(this.cue.points[i].fccChunk),
345
        pack(this.cue.points[i].dwChunkStart, this.uInt32),
346
        pack(this.cue.points[i].dwBlockStart, this.uInt32),
347
        pack(this.cue.points[i].dwSampleOffset, this.uInt32));
348
    }
349
    return points;
350
  }
351
352
  /**
353
   * Return the bytes of the 'smpl' chunk.
354
   * @return {!Array<number>} The 'smpl' chunk bytes.
355
   * @private
356
   */
357
  getSmplBytes_() {
358
    /** @type {!Array<number>} */
359
    let bytes = [];
360
    if (this.smpl.chunkId) {
361
      /** @type {!Array<number>} */
362
      let smplLoopsBytes = this.getSmplLoopsBytes_();
363
      bytes = bytes.concat(
364
        packString(this.smpl.chunkId),
365
        pack(smplLoopsBytes.length + 36, this.uInt32),
366
        pack(this.smpl.dwManufacturer, this.uInt32),
367
        pack(this.smpl.dwProduct, this.uInt32),
368
        pack(this.smpl.dwSamplePeriod, this.uInt32),
369
        pack(this.smpl.dwMIDIUnityNote, this.uInt32),
370
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32),
371
        pack(this.smpl.dwSMPTEFormat, this.uInt32),
372
        pack(this.smpl.dwSMPTEOffset, this.uInt32),
373
        pack(this.smpl.dwNumSampleLoops, this.uInt32),
374
        pack(this.smpl.dwSamplerData, this.uInt32),
375
        smplLoopsBytes);
376
    }
377
    return bytes;
378
  }
379
380
  /**
381
   * Return the bytes of the 'smpl' loops.
382
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
383
   * @private
384
   */
385
  getSmplLoopsBytes_() {
386
    /** @type {!Array<number>} */
387
    let loops = [];
388
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
389
      loops = loops.concat(
390
        pack(this.smpl.loops[i].dwName, this.uInt32),
391
        pack(this.smpl.loops[i].dwType, this.uInt32),
392
        pack(this.smpl.loops[i].dwStart, this.uInt32),
393
        pack(this.smpl.loops[i].dwEnd, this.uInt32),
394
        pack(this.smpl.loops[i].dwFraction, this.uInt32),
395
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32));
396
    }
397
    return loops;
398
  }
399
400
  /**
401
   * Return the bytes of the 'fact' chunk.
402
   * @return {!Array<number>} The 'fact' chunk bytes.
403
   * @private
404
   */
405
  getFactBytes_() {
406
    /** @type {!Array<number>} */
407
    let bytes = [];
408
    if (this.fact.chunkId) {
409
      bytes = bytes.concat(
410
        packString(this.fact.chunkId),
411
        pack(this.fact.chunkSize, this.uInt32),
412
        pack(this.fact.dwSampleLength, this.uInt32));
413
    }
414
    return bytes;
415
  }
416
417
  /**
418
   * Return the bytes of the 'fmt ' chunk.
419
   * @return {!Array<number>} The 'fmt' chunk bytes.
420
   * @throws {Error} if no 'fmt ' chunk is present.
421
   * @private
422
   */
423
  getFmtBytes_() {
424
    /** @type {!Array<number>} */
425
    let fmtBytes = [];
426
    if (this.fmt.chunkId) {
427
      return fmtBytes.concat(
428
        packString(this.fmt.chunkId),
429
        pack(this.fmt.chunkSize, this.uInt32),
430
        pack(this.fmt.audioFormat, this.uInt16),
431
        pack(this.fmt.numChannels, this.uInt16),
432
        pack(this.fmt.sampleRate, this.uInt32),
433
        pack(this.fmt.byteRate, this.uInt32),
434
        pack(this.fmt.blockAlign, this.uInt16),
435
        pack(this.fmt.bitsPerSample, this.uInt16),
436
        this.getFmtExtensionBytes_());
437
    }
438
    throw Error('Could not find the "fmt " chunk');
439
  }
440
441
  /**
442
   * Return the bytes of the fmt extension fields.
443
   * @return {!Array<number>} The fmt extension bytes.
444
   * @private
445
   */
446
  getFmtExtensionBytes_() {
447
    /** @type {!Array<number>} */
448
    let extension = [];
449
    if (this.fmt.chunkSize > 16) {
450
      extension = extension.concat(
451
        pack(this.fmt.cbSize, this.uInt16));
452
    }
453
    if (this.fmt.chunkSize > 18) {
454
      extension = extension.concat(
455
        pack(this.fmt.validBitsPerSample, this.uInt16));
456
    }
457
    if (this.fmt.chunkSize > 20) {
458
      extension = extension.concat(
459
        pack(this.fmt.dwChannelMask, this.uInt32));
460
    }
461
    if (this.fmt.chunkSize > 24) {
462
      extension = extension.concat(
463
        pack(this.fmt.subformat[0], this.uInt32),
464
        pack(this.fmt.subformat[1], this.uInt32),
465
        pack(this.fmt.subformat[2], this.uInt32),
466
        pack(this.fmt.subformat[3], this.uInt32));
467
    }
468
    return extension;
469
  }
470
471
  /**
472
   * Return the bytes of the 'LIST' chunk.
473
   * @return {!Array<number>} The 'LIST' chunk bytes.
474
   * @private
475
   */
476
  getLISTBytes_() {
477
    /** @type {!Array<number>} */
478
    let bytes = [];
479
    for (let i=0; i<this.LIST.length; i++) {
480
      /** @type {!Array<number>} */
481
      let subChunksBytes = this.getLISTSubChunksBytes_(
482
          this.LIST[i].subChunks, this.LIST[i].format);
483
      bytes = bytes.concat(
484
        packString(this.LIST[i].chunkId),
485
        pack(subChunksBytes.length + 4, this.uInt32),
486
        packString(this.LIST[i].format),
487
        subChunksBytes);
488
    }
489
    return bytes;
490
  }
491
492
  /**
493
   * Return the bytes of the sub chunks of a 'LIST' chunk.
494
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
495
   * @param {string} format The format of the 'LIST' chunk.
496
   *    Currently supported values are 'adtl' or 'INFO'.
497
   * @return {!Array<number>} The sub chunk bytes.
498
   * @private
499
   */
500
  getLISTSubChunksBytes_(subChunks, format) {
501
    /** @type {!Array<number>} */
502
    let bytes = [];
503
    for (let i=0; i<subChunks.length; i++) {
504
      if (format == 'INFO') {
505
        bytes = bytes.concat(
506
          packString(subChunks[i].chunkId),
507
          pack(subChunks[i].value.length + 1, this.uInt32),
508
          writeString(
509
            subChunks[i].value, subChunks[i].value.length));
510
        bytes.push(0);
511
      } else if (format == 'adtl') {
512
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
513
          bytes = bytes.concat(
514
            packString(subChunks[i].chunkId),
515
            pack(
516
              subChunks[i].value.length + 4 + 1, this.uInt32),
517
            pack(subChunks[i].dwName, this.uInt32),
518
            writeString(
519
              subChunks[i].value,
520
              subChunks[i].value.length));
521
          bytes.push(0);
522
        } else if (subChunks[i].chunkId == 'ltxt') {
523
          bytes = bytes.concat(
524
            this.getLtxtChunkBytes_(subChunks[i]));
525
        }
526
      }
527
      if (bytes.length % 2) {
528
        bytes.push(0);
529
      }
530
    }
531
    return bytes;
532
  }
533
534
  /**
535
   * Return the bytes of a 'ltxt' chunk.
536
   * @param {!Object} ltxt the 'ltxt' chunk.
537
   * @private
538
   */
539
  getLtxtChunkBytes_(ltxt) {
540
    return [].concat(
541
      packString(ltxt.chunkId),
542
      pack(ltxt.value.length + 20, this.uInt32),
543
      pack(ltxt.dwName, this.uInt32),
544
      pack(ltxt.dwSampleLength, this.uInt32),
545
      pack(ltxt.dwPurposeID, this.uInt32),
546
      pack(ltxt.dwCountry, this.uInt16),
547
      pack(ltxt.dwLanguage, this.uInt16),
548
      pack(ltxt.dwDialect, this.uInt16),
549
      pack(ltxt.dwCodePage, this.uInt16),
550
      writeString(ltxt.value, ltxt.value.length));
551
  }
552
553
  /**
554
   * Return the bytes of the 'junk' chunk.
555
   * @private
556
   */
557
  getJunkBytes_() {
558
    /** @type {!Array<number>} */
559
    let bytes = [];
560
    if (this.junk.chunkId) {
561
      return bytes.concat(
562
        packString(this.junk.chunkId),
563
        pack(this.junk.chunkData.length, this.uInt32),
564
        this.junk.chunkData);
565
    }
566
    return bytes;
567
  }
568
569
  /**
570
   * Validate the bit depth.
571
   * @return {boolean} True is the bit depth is valid.
572
   * @throws {Error} If bit depth is invalid.
573
   * @private
574
   */
575
  validateBitDepth_() {
576
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
577
      if (parseInt(this.bitDepth, 10) > 8 &&
578
          parseInt(this.bitDepth, 10) < 54) {
579
        return true;
580
      }
581
      throw new Error('Invalid bit depth.');
582
    }
583
    return true;
584
  }
585
}
586